// $Id: ViewScreen.java 15 2009-01-12 17:40:53Z moyekj@yahoo.com $

package com.hme.tivo.videostream;

import com.tivo.hme.bananas.BApplication;
import com.tivo.hme.sdk.StreamResource;
import com.tivo.hme.sdk.HmeEvent;
import com.tivo.hme.bananas.BEvent;
import java.util.Enumeration;
import java.io.File;
import java.io.BufferedReader;
import java.io.InputStreamReader;
import java.io.BufferedWriter;
import java.io.FileWriter;
import java.io.IOException;

import java.awt.Color;
import java.util.Date;
import java.util.Stack;

import java.util.Hashtable;
import java.util.Map;
import java.util.TreeMap;
import java.util.Iterator;


public class ViewScreen extends ScreenTemplate {
   String DIR;
   InitialScreen initialScreen;
   StreamResource stream;
   VText waitText;
   StatusBar statusBar;
   String NAME;
   int TEXT_HEIGHT = 20;
   int SPEEDS[] = {-60, -18, -3, 1, 3, 18, 60};
   int stream_speed = 3;
   long position = 0;
   long duration = 0;
   long ffmpeg_duration = 0;
   Boolean end_flag = false;
   long timeout_info = -1;
   long timeout_status = -1;
   long timeout_keypad = -1;
   long timeout_idle = -1;
   long timeout_time2byte = -1;
   Stack<Integer> keypad = new Stack<Integer>();
   VText infoText;
   VText errorText;
   VText keypadText;
        
   public ViewScreen(BApplication app, String name) {
      super(app);
      NAME = name;
   }

   public void startStream(String URI, String fullName, String fileName) {
      debug.print("URI=" + URI + " fileName=" + fileName);
      // File information screen text (for Info button)      
      MetaData m = new MetaData(fullName);
      if (m.exists()) {
         infoText = m.createInfoText(getScreen());
      } else {
         infoText = new VText(
               getNormal(), SAFE_ACTION_H, SAFE_ACTION_V, 2, "small"
         );
         String info = fileName;
         int index = GLOBAL.playList_index + 1;
         if (GLOBAL.playList_index > -1) {
            info = "Playlist item " +
               index +
               "/" +
               GLOBAL.playList.size() +
               "\n" +
               info;
         }
         infoText.setValue(info);
      }
      infoText.setVisible(false);

      // keypad text (when numbers pressed)
      keypadText = new VText(
         getNormal(), SAFE_ACTION_H, GLOBAL.statusBG_Y+20, 1, ""
      );
      keypadText.setVisible(false);

      // Error screen text (for error reporting)
      errorText = new VText(
         getNormal(), SAFE_ACTION_H, getHeight()-getHeight()/3, 6, "small"
      );
      //errorText.setFlags(RSRC_HALIGN_CENTER | RSRC_TEXT_WRAP);
      errorText.setFlags(RSRC_HALIGN_CENTER | RSRC_TEXT_WRAP);
      errorText.setColor(Color.red);
      errorText.setVisible(false);

      // Determine length of title using ffmpeg
      ffmpeg_duration = ffmpegGetDuration(fullName);
      //System.out.println("ffmpeg duration: " + ffmpeg_duration);

      // Start the stream
      GLOBAL.playList_lock = true;
      String mime = getMime(fileName);
      stream = createStream(URI,mime,true);
      getBelow().setResource(stream);
      waitText = new VText(
         getBelow(), SAFE_ACTION_H, getHeight()/2, 3, "small"
      );
      waitText.setFlags(RSRC_HALIGN_CENTER | RSRC_TEXT_WRAP);
      String message = "Starting " + fileName + " ...";
      if (GLOBAL.playList_index > -1) {
         int i = GLOBAL.playList_index + 1;
         message = message.concat(
            "\nPlaylist item " + i + "/" + GLOBAL.playList.size()
         );
      }
      waitText.setValue(message);
   }
   
   public void stopStream(BApplication app, int playList_index) {
      debug.print("app=" + app + " playList_index=" + playList_index);
      if (stream != null) {
         // Close & remove stream
         stream.close();
         stream.remove();
         stream = null;
         try {
            GLOBAL.cstream.close();
            GLOBAL.cstream = null;
         } catch (IOException e) {
            e.printStackTrace();
            System.out.println("Had trouble closing cstream");
         } 
   
         // Update positions hash with closest file byte size
         // corresponding to the current position in time
         if (updatePositionsHash(position)) {
            // Save updates to bookmark file
            saveBookmarks();
         }
                        
         // Clear time2byte hash to prepare for next stream
         GLOBAL.time2byte.clear();
      
         // Pop back to file browsing mode
         app.pop();
         play("left.snd");
   
         GLOBAL.playList_index = playList_index;         
         if (playList_index >= 0) {
            // Go to playList item in file browser mode then start playback
            playList_start(playList_index);
         } else {
            // Re-refresh directory listing (re-build ViewScreen list)
            initialScreen.updateFileList(DIR);
            initialScreen.focusOn(NAME);
         }
      }
   }
   
   public void playList_start(int playList_index) {
      debug.print("playList_index=" + playList_index);
      // Go to playList item in file browser mode then start playback
      String fullName = GLOBAL.playList.get(playList_index);
      File f = new File(fullName);
      String fileName = f.getName();
      String dir = f.getParent();
      System.out.println("Playlist item " + playList_index + ": " + fullName);
      DIR = dir;
      NAME = fileName;
      initialScreen.updateFileList(DIR);
      initialScreen.focusOn(NAME);
      initialScreen.postEvent(new BEvent.Action(initialScreen, "push"));      
   }

   // Define mime types based on special file extensions
   public String getMime(String fileName) {
      debug.print("fileName=" + fileName);
      String mime = null;
      // DVD mpeg2 vob files
      if (fileName.toLowerCase().endsWith(".vob")) {
         mime = "video/mpeg";
      }
      return mime;
   }
    
   public void changeSpeed(float speed) {
      debug.print("speed=" + speed);
      stream.setSpeed(speed);
      displayStatusBar(true);
   }
    
   public void displayStatusBar(Boolean state) {
      debug.print("state=" + state);
      if (statusBar == null) { return; }
      if (state == true) {
         timeout_status = new Date().getTime() + 1000*GLOBAL.timeout_status_bar;
         statusBar.makeVisible(true);
      } else {
      timeout_status = -1;
      statusBar.makeVisible(false);
      }
   }
   
   public void displayError(String error) {
      debug.print("error=" + error);
      errorText.setValue(error);
      errorText.setVisible(true);
   }

   public boolean handleEvent(HmeEvent event) {
      //debug.print("event=" + event);
      // Clear text if timeouts reached
      long date = new Date().getTime();
      if (timeout_info > 0 && date >= timeout_info) {
         infoText.setVisible(false);
         timeout_info = -1;
      }
      if (timeout_status > 0 && date >= timeout_status) {
         statusBar.makeVisible(false);
         timeout_status = -1;
      }
      if (timeout_keypad > 0 && date >= timeout_keypad) {
         keypadText.setVisible(false);
         timeout_keypad = -1;
         keypad.removeAllElements();
      }

      // Prevent timeout by sending acknowledge signal every few mins
      if (date > timeout_idle) {
         getApp().acknowledgeIdle(true);
         timeout_idle = date + 4000;
      }
      
      // Update stream position and duration information
      if ( event instanceof HmeEvent.ResourceInfo ){
         HmeEvent.ResourceInfo info = (HmeEvent.ResourceInfo) event;
         position = Long.parseLong(info.getMap().get("position").toString());
         duration = Long.parseLong(info.getMap().get("duration").toString());
         if (stream != null) {
            int status = stream.getStatus();
            // 0 => RSRC_STATUS_UNKNOWN
            // 1 => RSRC_STATUS_CONNECTING
            // 2 => RSRC_STATUS_CONNECTED
            // 3 => RSRC_STATUS_LOADING
            // 4 => RSRC_STATUS_READY
            // 5 => RSRC_STATUS_PLAYING
            // 6 => RSRC_STATUS_PAUSED
            // 7 => RSRC_STATUS_SEEKING
            // 8 => RSRC_STATUS_CLOSED
            // 9 => RSRC_STATUS_COMPLETE
            // 10 => RSRC_STATUS_ERROR
            // 11 => ? (playback completed - only for mp4)
            // 12 => ? (playback caught up to buffer)
            //System.out.println("status=" + status);
                     
            if (status >= RSRC_STATUS_ERROR || status == RSRC_STATUS_CLOSED) {
               GLOBAL.playList_lock = false;
               // Display error associated with starting a stream
               if (status == RSRC_STATUS_ERROR) {
                  displayError(
                     "ERROR: " + info.getMap().get("error.text") + "\n" +
                     infoText.getValue().toString()
                  );
               }
               
               changeSpeed(0);               
               if (status == 11 || (status == 12 && ffmpeg_duration > 0 && position >= ffmpeg_duration - 5000)) {
                  // Playback completed, so stop stream
                  // This only works for mp4 right now
                  // Must use channel down for mpg
                  if (GLOBAL.playList_index > -1) {
                     GLOBAL.playList_index++;
                  }
                  if (GLOBAL.playList_index >= GLOBAL.playList.size()) {
                     GLOBAL.playList_index = -1;
                  }
                  stopStream(getBApp(), GLOBAL.playList_index);
                  return false;
               }
            }               
         
            if (status == RSRC_STATUS_READY) {
               GLOBAL.playList_lock = false;
               // Clear out Starting text and display status bar
               waitText.clearResource();
               waitText.remove();
               if (statusBar == null) {
                  statusBar = new StatusBar(getBelow());
                  displayStatusBar(true);
               }
            }
            if (status >= RSRC_STATUS_PLAYING && statusBar != null) {
               // Update status bar even when not being displayed
               statusBar.Update(position, duration);            
            }         
            
            // Make a time position->bytes entry in hash every 1 sec
            if (GLOBAL.cstream != null && timeout_time2byte < date) {
               timeout_time2byte = date + 1000;
               GLOBAL.time2byte.put(duration, GLOBAL.cstream.getCount());
            }
         }
      }
            
      return super.handleEvent(event);
   }

   public boolean handleKeyPress(int code, long rawcode) {
      debug.print("code=" + code + " rawcode=" + rawcode);
      if (stream == null) {
         // Directory mode
         switch (code) {
            case KEY_LEFT:
               // pop back to calling screen
               getBApp().pop();
               this.remove();
               return true;
         }
      } else {
         int mins;
         // Streaming video mode
         switch (code) {
            case KEY_LEFT:
               // Close and remove stream
               if (GLOBAL.playList_lock == false) {
                  stopStream(getBApp(), -1);
               }
               return true;
            case KEY_PAUSE:
               if (stream.getStatus() >= RSRC_STATUS_PLAYING) {
                  // Toggle between pause and play modes
                  if(stream.isPaused()) {
                     stream.play();
                     displayStatusBar(true);
                  } else {
                     stream.pause();
                     timeout_status = -1;
                     statusBar.makeVisible(true); // keep status bar displayed
                  }
               }
               return true;
            case KEY_PLAY:
               if (stream.getStatus() >= RSRC_STATUS_PLAYING) {
                  if (statusBar.visible && stream.getSpeed() == 1) {
                     displayStatusBar(false);
                  } else {
                     changeSpeed(1);
                     stream_speed = 3;
                  }
               }
               return true;
            case KEY_REVERSE:
               if (stream.getStatus() >= RSRC_STATUS_PLAYING) {
                  stream_speed -= 1;
                  if (stream_speed > -1) {
                     changeSpeed(SPEEDS[stream_speed]);
                  } else {
                     changeSpeed(1);
                     stream_speed = 3;
                  }
                  if (stream_speed < 0)
                     stream_speed = 0;
                  if (stream_speed == 2 || stream_speed == 4)
                     play("speedup1.snd");
                  if (stream_speed == 1 || stream_speed == 5)
                     play("speedup2.snd");
                  if (stream_speed == 0 || stream_speed == 6)
                     play("speedup3.snd");
                  if (stream_speed == 3)
                     play("slowdown1.snd");
               }
               return true;
            case KEY_FORWARD:
               if (stream.getStatus() >= RSRC_STATUS_PLAYING) {
                  stream_speed += 1;
                  if (stream_speed < 7) {
                     changeSpeed(SPEEDS[stream_speed]);
                  } else {
                     changeSpeed(1);
                     stream_speed = 3;
                  }
                  if (stream_speed > 6)
                     stream_speed = 6;
                  if (stream_speed == 2 || stream_speed == 4)
                     play("speedup1.snd");
                  if (stream_speed == 1 || stream_speed == 5)
                     play("speedup2.snd");
                  if (stream_speed == 0 || stream_speed == 6)
                     play("speedup3.snd");
                  if (stream_speed == 3)
                     play("slowdown1.snd");
               }
               return true;
            case KEY_REPLAY:
               if (stream.getStatus() >= RSRC_STATUS_PLAYING) {
                  mins = getMinutes();
                  if (mins > 0) {
                     // Jump n minutes back
                     stream.setPosition(position - mins*60000);
                     keypadText.setVisible(false);
                  } else {
                     // Instant Replay
                     stream.setPosition(position - 1000*GLOBAL.skip_back);
                  }
                  // Clear keypad numbers
                  keypadText.setVisible(false);
                  timeout_keypad = -1;
                  keypad.removeAllElements();
                  // Display status bar
                  displayStatusBar(true);
                  play("left.snd");
               }
               return true;
            case KEY_ADVANCE:
               if (stream.getStatus() >= RSRC_STATUS_PLAYING) {
                  mins = getMinutes();
                  if (mins > 0) {
                     // Jump n minutes forward
                     stream.setPosition(position + mins*60000);
                     keypadText.setVisible(false);
                  } else {
                     // Skip forwards
                     stream.setPosition(position + 1000*GLOBAL.skip_forwards);
                  }
                  // Clear keypad numbers
                  keypadText.setVisible(false);
                  timeout_keypad = -1;
                  keypad.removeAllElements();
                  // Display status bar
                  displayStatusBar(true);
                  play("right.snd");
               }
               return true;
            case KEY_SLOW:
               if (stream.getStatus() >= RSRC_STATUS_PLAYING) {
                  changeSpeed(GLOBAL.slow_speed);
               }
               return true;
            case KEY_CLEAR:
               // Clear text and status bar
               infoText.setVisible(false);
               displayStatusBar(false);
               return true;
            case KEY_INFO:
               // Display file name temporarily
               if (timeout_info == -1) {
                  timeout_info = new Date().getTime() + 1000*GLOBAL.timeout_info;
                  infoText.setVisible(true);
               } else {
                  infoText.setVisible(false);
                  timeout_info = -1;
               }
               play("select.snd");
               return true;
            case KEY_CHANNELDOWN:
               if (GLOBAL.playList_lock == false && GLOBAL.playList_index > -1) {
                  // Advance 1 forward in playlist if possible
                  int playList_index = GLOBAL.playList_index;
                  if (playList_index + 1 < GLOBAL.playList.size()) {
                     changeSpeed(0);
                     stopStream(getBApp(), playList_index+1);
                  }
               }
               return true;
            case KEY_CHANNELUP:
               if (GLOBAL.playList_lock == false && GLOBAL.playList_index > -1) {
                   // Advance 1 backwards in playlist if possible
                  int playList_index = GLOBAL.playList_index;
                  if (playList_index - 1 > -1) {
                     changeSpeed(0);
                     stopStream(getBApp(), playList_index-1);
                  }
               }
               return true;
            case KEY_NUM0:
               addKey(0);
               play("updown.snd");
               return true;
            case KEY_NUM1:
               addKey(1);
               play("updown.snd");
               return true;
            case KEY_NUM2:
               addKey(2);
               play("updown.snd");
               return true;
            case KEY_NUM3:
               addKey(3);
               play("updown.snd");
               return true;
            case KEY_NUM4:
               addKey(4);
               play("updown.snd");
               return true;
            case KEY_NUM5:
               addKey(5);
               play("updown.snd");
               return true;
            case KEY_NUM6:
               addKey(6);
               play("updown.snd");
               return true;
            case KEY_NUM7:
               addKey(7);
               play("updown.snd");
               return true;
            case KEY_NUM8:
               addKey(8);
               play("updown.snd");
               return true;
            case KEY_NUM9:
               addKey(9);
               play("updown.snd");
               return true;
         } // switch
      } // else
      return super.handleKeyPress(code, rawcode);
   }
   
   public void addKey(int num) {
      debug.print("num=" + num);
      timeout_keypad = new Date().getTime() + 5000;
      if (keypad.size() < 3) {
         keypad.push(num);
      }
      String val = "";
      for (int i=0; i<keypad.size(); ++i) {
         val = String.format("%s%d", val, keypad.get(i));
      }
      keypadText.setValue(val);
      keypadText.setVisible(true);
      displayStatusBar(true);
   }
   
   public int getMinutes() {
      debug.print("");
      int mins = 0;
      if (timeout_keypad != -1) {
         int multiplier = 1;
         for (int i=keypad.size()-1; i>=0; i--) {
            mins += keypad.get(i)*multiplier;
            multiplier *= 10;
         }
      }
      return mins;
   }

   // Find closest time less than or equal to given time_position
   // saved in time2byte hash and then update positions hash with
   // the corresponding # file bytes   
   public boolean updatePositionsHash(Long time_position) {
      debug.print("time_position=" + time_position);
      // Find time2byte hash key closest to but less than time_position
      time_position -= 5000; // Back up about 5 seconds
      if (time_position < 0) {
         // Remove bookmark (if any)
         if (GLOBAL.streamFile != null) {
            GLOBAL.positions.remove(GLOBAL.streamFile);
         }
         return true;
      }
      // This only prints hash table in debug mode
      printSortedHash("time2byte hash:", GLOBAL.time2byte);
      boolean found = false;
      while ( ! found && time_position > 0 ) {
         if ( GLOBAL.time2byte.containsKey(time_position) ) {
            found = true;
         } else {
            //System.out.println("--Tried time = " + time_position);
            time_position -= 1;
         }
      }
      if (found) {
         long bytes = GLOBAL.time2byte.get(time_position);
         debug.print("--Updated position: time=" + time_position + " bytes=" + bytes);
         GLOBAL.positions.remove(GLOBAL.streamFile);
         GLOBAL.positions.put(GLOBAL.streamFile, bytes);
      }
      return found;
   }

   // Update bookmarks file with latest positions hash contents
   public void saveBookmarks() {
      debug.print("");
      // Open bookmark.tmp file in write mode
      BufferedWriter bookmark;
      try {
         FileWriter ofp = new FileWriter(GLOBAL.bookmarkFile + ".tmp");
         bookmark = new BufferedWriter(ofp);
         // Write bookmarks to tmp file
         if (GLOBAL.positions != null) {
            Enumeration<String> e = GLOBAL.positions.keys();
            String key;
            while (e.hasMoreElements()) {
               key = e.nextElement();
               try {
                  bookmark.write(
                        key + "__##__" + GLOBAL.positions.get(key) + "\n"
                  );
               } catch (IOException ex) {
                  ex.printStackTrace();
               }
            }
            try {
               bookmark.close();
            } catch (IOException ex) {
               ex.printStackTrace();
            }
         
            // Replace bookmark with bookmark.tmp
            File file = new File(GLOBAL.bookmarkFile);
            file.delete();
            File tmpfile = new File(GLOBAL.bookmarkFile + ".tmp");
            tmpfile.renameTo(file);
         }
         File tmp = new File(GLOBAL.bookmarkFile + ".tmp");
         tmp.delete();
      }
      catch (Exception e) {
         e.printStackTrace();
      }
   }

   public void remove() {
      debug.print("");
      if (statusBar != null) statusBar.remove();
      if (stream != null) {
         stream.remove();
      }
      if (waitText != null) {
         waitText.setValue(null);
         waitText.clearResource();
         waitText.remove();
      }
      if (infoText != null) {
         infoText.setValue(null);
         infoText.clearResource();
         infoText.remove();
      }
      if (errorText != null) {
         errorText.setValue(null);
         errorText.clearResource();
         errorText.remove();
      }
      if (keypadText != null) {
         keypadText.setValue(null);
         keypadText.clearResource();
         keypadText.remove();
      }
      super.remove();
  }
   
   public void finalize() {
      debug.print("");
      this.remove();
      try {
         super.finalize();
      } catch (Throwable e) {
         e.printStackTrace();
      }
   }

   public String toString() {
      return NAME;
   }
   
   @SuppressWarnings("unchecked")
   public void printSortedHash(String prefix, Hashtable hash) {
      TreeMap sorted = new TreeMap( hash );
      debug.print(prefix);
      for ( Iterator it = sorted.entrySet().iterator(); it.hasNext(); ) {
          Map.Entry e = (Map.Entry) it.next();
          debug.print( e.getKey() + "=" + e.getValue() );
      }
   }

   public Stack<String> CmdExec(String cmdline) {
     debug.print("cmdline=" + cmdline);
     Stack<String> stderr = new Stack<String>();
     try {
      String line;
      Process p = Runtime.getRuntime().exec(cmdline);
      BufferedReader input = 
        new BufferedReader
          (new InputStreamReader(p.getErrorStream()));
      while ((line = input.readLine()) != null) {
        stderr.add(line);
      }
      input.close();
     } 
     catch (Exception err) {
      err.printStackTrace();
     }
     return stderr;
   }
   
   public long ffmpegGetDuration(String file) {
      debug.print("file=" + file);
      long time = 0;
      File f = new File(GLOBAL.ffmpeg);
      if (f.exists() == false) {
         return time;
      }
      Stack<String> result = new Stack<String>();
      result = CmdExec(GLOBAL.ffmpeg + " -i \"" + file + "\"");
      for(int i=0; i<result.size(); i++) {
         if (result.get(i).matches("^\\s+Duration:.+")) {
            String l[] = result.get(i).split("\\s+");
            String l1[] = l[2].replace(",","").split(":");
            time = 3600*Integer.parseInt(l1[0]) +
                   60*Integer.parseInt(l1[1]) +
                   (int)Float.parseFloat(l1[2]);
            time = time*1000;
         }
      }
      return time;
   }
}
